#!/usr/bin/env python
#
# Simulator components for virtual machines and virtualization systems
#
# Author:   Mike Murphy <mamurph@cs.clemson.edu>
# Revision: 23 September 2009
#
#   Copyright 2009 Clemson University
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#
# This material is based upon work supported under a National Science
# Foundation Graduate Research Fellowship. Any opinions, findings,
# conclusions or recommendations expressed in this publication are those of
# the author(s) and do not necessarily reflect the views of the National
# Science Foundation.


'''
Components for simulating virtual machines and the physical hosts that
support virtualization. Virtual machines may add some amount of execution
overhead, in terms of increased length, to a job. This overhead depends
upon the fraction of the job that is CPU bound and the fraction that is
network bound, with a linear interpolation of overhead values between the
two bounds. Full network-bound overhead is reached whenever the network
fraction of the job reaches a specified bounding threshold (the default
is 50%).
'''

from cluster_sim import Machine
from core_common import PJ_STOP
from results import VMResult
from storage import DataFile


class VirtualMachine(Machine):
   '''
   Representation of a virtual machine (guest system) *instance*. The VM may
   add execution overhead to a job, resulting in a longer execution length.
   '''
   def __init__(self, name, image, state_callback=None, cpu_overhead=0.0, \
                net_overhead=0.0, net_bound=50, **kwds):
      '''
      @param name: Name of the VM instance
      @type name: str
      @param image: VMImage used to spawn the VM instance
      @param state_callback: Machine state callback (see
                             L{cluster_sim.Machine})
      @param cpu_overhead: Amount of overhead, expressed as a floating-point
                           value (e.g. 0.1 for 10%), added to CPU-bound jobs
                           that have a network fraction of 0%
      @type cpu_overhead: float
      @param net_overhead: Amount of overhead, expressed as a floating-point
                           value, added to network-bound jobs that have a
                           network fraction of at least net_bound percent
      @type net_overhead: float
      @param net_bound:    Percentage of network fractional level (100 - cpu)
                           at which a job is considered to be network-bound.
                           This value is expressed as a floating-point or
                           integer whole percentage number (e.g. 10 for 10%)
      @type net_bound: number
      @param kwds: Extra data for the Machine instance
      '''
      Machine.__init__(self, name, state_callback, **kwds)
      self.image = image
      self.cpu_overhead = cpu_overhead
      self.net_overhead = net_overhead
      self.net_bound = net_bound
      self.booted = False
   def booted_evt(self, time, sim):
      '''
      Receives a simulator event when the VM is in the booted state (after
      a boot delay specified as a property of the image). If the VM has not
      been killed, it will set its state to "free" (resulting in a callback,
      if the callback function was set in the constructor) and then join
      the scheduler instance specified by the VM image.

      @param time: Time at which VM has booted
      @type time: number
      @param sim: Reference to the simulator object

      @return: simulator result collection
      '''
      r = []
      if self.alive:
         self.free()
         self.image.sched.add_target(self)
         r.append(self.image.sched.get_update())
         self.booted = True
         r.append(VMResult(time, self.name, 'booted'))
      else:
         r.append(self.shut_down(time, sim))
         r.append(VMResult(time, self.name, 'shutdown'))
      return (r,)
   def notify_shutdown_evt(self, time, sim, signum=PJ_STOP):
      '''
      Receives an event from the simulator when the VM instance is to be
      shut down. A shutdown can be "normal" or "abnormal". In a normal
      shutdown, the VM is removed from the scheduler pool of the associated
      scheduler. In the case of an abnormal shutdown, the VM is *not* removed
      from the scheduler pool, which will result in scheduling inefficiencies
      similar to those that would be experienced on a real scheduler when a
      target node fails unexpectedly.

      @param time: Termination time
      @type time: number
      @param sim: Reference to the simulator object
      @param signum: Signal number (PJ_STOP for normal shutdown, any other
                     value for an abnormal shutdown -- see L{core_common})
      @type signum: int

      @return: simulator result collection
      '''
      r = []
      if self.alive and self.booted:
         if signum == PJ_STOP:
            self.image.sched.remove_target(self)
         r.append(self.image.sched.get_update())
         r.append(self.shut_down(time, sim))
         r.append(VMResult(time, self.name, 'shutdown'))
         self.booted = False
      else:
         self.alive = False
         r.append(VMResult(time, self.name, 'abort'))
      return (r,)
   def adjust_length(self, length, cpu, net):
      '''
      Adjusts the length of a job at start time, in order to account for the
      overhead added by virtualization. The CPU and network levels of the job
      are used, along with the overhead parameters specified in the
      constructor, to increase the job length by some fractional amount. A
      linear interpolation is performed using the CPU-bound overhead,
      network-bound overhead, and network-bound threshold parameters.

      @param length: Original length of the job
      @type length: number
      @param cpu: Percentage of job spent utilizing the CPU (whole
                  value, such as 10 for 10%, as an int or float)
      @type cpu: number
      @param net: Percentage of job spent sending and receiving
                  data over the network (also expressed as a whole
                  value, using an int or float). Note that there is
                  an unchecked assumption that cpu + net = 100
      @type net: number

      @rtype: number
      @return: newly adjusted length of the job
      '''
      base = self.net_overhead
      if self.net_bound > 0:
         netcpt = (self.net_overhead - self.cpu_overhead) / self.net_bound
         base = (netcpt * net) + self.cpu_overhead
      adj = 1 + min(base, self.net_overhead)
      result = int(length * adj)
      return result
#


class VMImage(DataFile):
   '''
   Simulated representation of a virtual machine image file, which is typically
   a large file (10 binary gigabytes or larger) containing a disk image used
   to run the virtual machine.
   '''
   def __init__(self, name, size, sched_to_join, boot_time, **kwds):
      '''
      @param name: Name of the VM image file
      @type name: str
      @param size: Size of the file in bytes
      @type size: int
      @param sched_to_join: Scheduler instance to which any VM instances
                            booted from this image will be added as targets
      @param boot_time: Amount of time required to boot a VM instance from
                        this image
      @type boot_time: number
      @param kwds: Image extra data
      '''
      DataFile.__init__(self, name, size, **kwds)
      self.sched = sched_to_join
      self.boot_time = boot_time
#


class VMMonitor(object):
   '''
   Representation of a Virtual Machine Monitor (VMM) or hypervisor, which
   runs on a simulated physical host and manages (starts and stops) simulated
   virtual machines.
   '''
   def __init__(self, name, sim, cpu_oh=0.0, net_oh=0.0, net_b=50, **kwds):
      '''
      @param name: Name of the VMM
      @type name: str
      @param sim: Reference to the simulator object
      @param cpu_oh: Virtualization overhead for processes that are
                     fully CPU bound, expressed as a decimal floating
                     point value (e.g. 0.1 for 10%)
      @type cpu_oh: float
      @param net_oh: Virtualization overhead for processes that are
                     fully network bound, expressed as a decimal
                     floating point value
      @type net_oh: float
      @param net_b: Threshold at which a process is considered fully
                    network bound, expressed as a whole percentage
                    number (e.g. 10 for 10%)
      @type net_b: number
      @param kwds: VMM extra data
      '''
      self.name = name
      self.sim = sim
      self.vmp = {'cpu_overhead': cpu_oh, 'net_overhead': net_oh, \
                  'net_bound': net_b}
      self.extra_data = kwds
   def boot_vm(self, vm_image, name, state_callback=None):
      '''
      Boots a new guest VM instance on the local host

      @param vm_image: VMImage object from which VM instance is to be created
      @param name: Machine name of the newly booted VM
      @type name: str
      @param state_callback: State callback for the VM (see
                             L{cluster_sim.Machine})

      @return: a reference to the VM instance that is being booted
      '''
      when = self.sim.get_time() + vm_image.boot_time
      kwds = vm_image.extra_data.copy()
      for key in self.vmp:
         kwds[key] = self.vmp[key]
      vm = VirtualMachine(name, vm_image, state_callback, **kwds)
      self.sim.new_simple_event(when, vm.booted_evt)
      return vm
   def shut_down_vm(self, vm, time_offset=0, signum=PJ_STOP):
      '''
      Terminates a VM instance by calling the shut_down() method of the VM

      @param vm: VM instance (object) to terminate
      @param time_offset: Offset from the current time at which the VM should
                          shut down
      @type time_offset: number
      @param signum: Signal number to send to the VM when terminating
                     (PJ_STOP for a normal shut down, PJ_KILL for
                     abnormal termination)
      @type signum: int
      '''
      when = self.sim.get_time() + time_offset
      self.sim.new_simple_event(when, vm.notify_shutdown_evt, signum)
#


class VMHost(Machine):
   '''
   Represents a physical machine that has a Virtual Machine Monitor installed
   and is capable of hosting virtual machines. Access to the VMM service is
   provided via the "vmm" capability of the Machine, which can be obtained by
   calling the get_capability() method.
   '''
   def __init__(self, name, vmm_name, sim, cpu_oh=0.0, net_oh=0.0, \
                net_bound=50, state_callback=None, **kwds):
      '''
      @param name: Name of the machine
      @type name: str
      @param vmm_name: Name of the VMM installed on the machine
      @type vmm_name: str
      @param sim: Reference to the simulator object
      @param cpu_oh: Virtualization overhead for processes that are
                     fully CPU bound, expressed as a decimal floating
                     point value (e.g. 0.1 for 10%)
      @type cpu_oh: float
      @param net_oh: Virtualization overhead for processes that are
                     fully network bound, expressed as a decimal
                     floating point value
      @type net_oh: float
      @param net_bound: Threshold at which a process is considered fully
                        network bound, expressed as a whole percentage
                        number (e.g. 10 for 10%)
      @type net_bound: number
      @param state_callback: Optional state callback for the *host* machine
                             (see L{cluster_sim.Machine} for more information)
      @param kwds: Host machine extra data. If the extra data include
                     a "classAds" keyword argument, then a special
                     classAd ({'IS_METAL': 'TRUE'}) will be added to
                     the supplied classAds.
      '''
      if kwds is None:
         kwds = {}
      if kwds.has_key('classAds'):
         kwds['classAds']['IS_METAL'] = 'TRUE'
      else:
         kwds['classAds'] = {'IS_METAL': 'TRUE'}
      Machine.__init__(self, name, state_callback, **kwds)
      vmm = VMMonitor(vmm_name, sim, cpu_oh, net_oh, net_bound)
      self.vmm = vmm
      self.add_capability('vmm', self.vmm)
#


if __name__ == '__main__':
   import kernel, scheduler, job_model, condor_sim

   sim = kernel.Simulator()
   sched = scheduler.Scheduler(sim)
   sched2 = condor_sim.Condor(sim)
   img = VMImage('image', 1000000, sched2, 30)

   def vm_callback(machine, state):
      print 'Machine %s in state %d' % (machine.name, state)

   def pilot_callback(job, time, sim, sched, node):
      vm = node.vmm.boot_vm(img, 'virtual', vm_callback)
      return vm
   #

   pm = new_vm_host('metal', 'KVM', sim)
   sched.add_target(pm)

   pilot = job_model.PilotJob(pilot_callback)
   actual = job_model.Job(100)

   sched.add_submit_event(30, pilot)
   sched2.add_submit_event(30, actual)

   # Without using a watchdog, we have no way to terminate the VM, so fake it
   # for test purposes
   def terminate_vm(time, simobj):
      pilot.notify_done(time, simobj)
   #
   sim.new_simple_event(10000, terminate_vm)

   sim.run()
#
